/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.calcite.linq4j.tree;

import org.checkerframework.checker.nullness.qual.Nullable;
import org.joou.UByte;
import org.joou.UInteger;
import org.joou.ULong;
import org.joou.UShort;
import org.joou.Unsigned;

import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;

/** Support for compiling unsigned types.
 * This class has a role similar to {@link Primitive} */
public enum UnsignedType {
  UBYTE,
  USHORT,
  UINT,
  ULONG;

  public static @Nullable UnsignedType of(Type type) {
    if (type == UByte.class) {
      return UBYTE;
    } else if (type == UShort.class) {
      return USHORT;
    } else if (type == UInteger.class) {
      return UINT;
    } else if (type == ULong.class) {
      return ULONG;
    } else {
      return null;
    }
  }

  public String toJavaTypeName() {
    switch (this) {
    case UBYTE:
      return UByte.class.getSimpleName();
    case USHORT:
      return UShort.class.getSimpleName();
    case UINT:
      return UInteger.class.getSimpleName();
    case ULONG:
      return ULong.class.getSimpleName();
    default:
      throw new AssertionError("Unexpected unsigned type " + this);
    }
  }

  /** Get the name of the function that casts a value of type Object
   * to the current type. */
  public String getConvertFunctionName() {
    return "to" + this.toJavaTypeName();
  }

  public static BigInteger toBigInteger(ULong value) {
    // This function is missing from the ULong class
    long l = value.longValue();
    if (l >= 0L) {
      return BigInteger.valueOf(l);
    }
    return BigInteger.valueOf(l & Long.MAX_VALUE).add(ULong.MAX_VALUE_LONG);
  }

  // Convert a value to an unsigned type

  // The following methods are called in code generated by EnumUtils.convert

  // UByte
  public static UByte toUByte(byte n) {
    return toUByteNonNull(n, RoundingMode.DOWN);
  }

  public static UByte toUByte(short n) {
    return toUByteNonNull(n, RoundingMode.DOWN);
  }

  public static UByte toUByte(int n) {
    return toUByteNonNull(n, RoundingMode.DOWN);
  }

  public static UByte toUByte(long n) {
    return toUByteNonNull(n, RoundingMode.DOWN);
  }

  public static UByte toUByte(float n) {
    return toUByteNonNull(n, RoundingMode.DOWN);
  }

  public static UByte toUByte(double n) {
    return toUByteNonNull(n, RoundingMode.DOWN);
  }

  private static UByte toUByteNonNull(Number n, RoundingMode mode) {
    if (n instanceof BigDecimal) {
      BigDecimal d = ((BigDecimal) n).setScale(0, mode);
      return Unsigned.ubyte(d.longValueExact());
    } else if (n instanceof Byte || n instanceof Short || n instanceof Integer
        || n instanceof Long || n instanceof Float || n instanceof Double
        || n instanceof UByte || n instanceof UShort
        || n instanceof UInteger || n instanceof ULong) {
      // Using long ensures a range check
      return Unsigned.ubyte(n.longValue());
    }
    throw new IllegalArgumentException("Unexpected Number value " + n.getClass().getSimpleName());
  }

  public static @Nullable UByte toUByte(@Nullable Number n, RoundingMode mode) {
    if (n == null) {
      return null;
    }
    return toUByteNonNull(n, mode);
  }

  public static @Nullable UByte toUByte(@Nullable String s, RoundingMode unused) {
    if (s == null) {
      return null;
    }
    return Unsigned.ubyte(s);
  }

  // To UShort

  public static UShort toUShort(byte n) {
    return toUShortNonNull(n, RoundingMode.DOWN);
  }

  public static UShort toUShort(short n) {
    return toUShortNonNull(n, RoundingMode.DOWN);
  }

  public static UShort toUShort(int n) {
    return toUShortNonNull(n, RoundingMode.DOWN);
  }

  public static UShort toUShort(long n) {
    return toUShortNonNull(n, RoundingMode.DOWN);
  }

  public static UShort toUShort(float n) {
    return toUShortNonNull(n, RoundingMode.DOWN);
  }

  public static UShort toUShort(double n) {
    return toUShortNonNull(n, RoundingMode.DOWN);
  }

  private static UShort toUShortNonNull(Number n, RoundingMode mode) {
    if (n instanceof BigDecimal) {
      BigDecimal d = ((BigDecimal) n).setScale(0, mode);
      return Unsigned.ushort(d.intValueExact());
    } else if (n instanceof Byte || n instanceof Short || n instanceof Integer
        || n instanceof Long || n instanceof Float || n instanceof Double
        || n instanceof UByte || n instanceof UShort
        || n instanceof UInteger || n instanceof ULong) {
      long value = n.longValue();
      // ushort(long) is missing from the Unsigned class
      if (value < 0 || value > 65535) {
        throw new NumberFormatException("Value is out of range : " + n);
      }
      return Unsigned.ushort(n.intValue());
    }
    throw new IllegalArgumentException("Unexpected Number value " + n.getClass().getSimpleName());
  }

  public static @Nullable UShort toUShort(@Nullable Number n, RoundingMode mode) {
    if (n == null) {
      return null;
    }
    return toUShortNonNull(n, mode);
  }

  public static @Nullable UShort toUShort(@Nullable String s, RoundingMode unused) {
    if (s == null) {
      return null;
    }
    return Unsigned.ushort(s);
  }

  // UInteger
  public static UInteger toUInteger(byte n) {
    // Casting to long forces a range check
    return toUIntegerNonNull(n, RoundingMode.DOWN);
  }

  public static UInteger toUInteger(short n) {
    return toUIntegerNonNull(n, RoundingMode.DOWN);
  }

  public static UInteger toUInteger(int n) {
    return toUIntegerNonNull(n, RoundingMode.DOWN);
  }

  public static UInteger toUInteger(long n) {
    return toUIntegerNonNull(n, RoundingMode.DOWN);
  }

  public static UInteger toUInteger(float n) {
    return toUIntegerNonNull(n, RoundingMode.DOWN);
  }

  public static UInteger toUInteger(double n) {
    return toUIntegerNonNull(n, RoundingMode.DOWN);
  }

  private static UInteger toUIntegerNonNull(Number n, RoundingMode mode) {
    if (n instanceof BigDecimal) {
      BigDecimal d = ((BigDecimal) n).setScale(0, mode);
      return Unsigned.uint(d.longValueExact());
    } else if (n instanceof Byte || n instanceof Short || n instanceof Integer
        || n instanceof Long || n instanceof Float || n instanceof Double
        || n instanceof UByte || n instanceof UShort
        || n instanceof UInteger || n instanceof ULong) {
      // Using long ensures a range check
      return Unsigned.uint(n.longValue());
    }
    throw new IllegalArgumentException("Unexpected Number value " + n.getClass().getSimpleName());
  }

  public static @Nullable UInteger toUInteger(@Nullable Number n, RoundingMode mode) {
    if (n == null) {
      return null;
    }
    return toUIntegerNonNull(n, mode);
  }

  public static @Nullable UInteger toUInteger(@Nullable String s, RoundingMode unused) {
    if (s == null) {
      return null;
    }
    return Unsigned.uint(s);
  }

  // ULong
  public static ULong toULong(byte n) {
    // Casting to long forces a range check
    return toULongNonNull(n, RoundingMode.DOWN);
  }

  public static ULong toULong(short n) {
    return toULongNonNull(n, RoundingMode.DOWN);
  }

  public static ULong toULong(int n) {
    return toULongNonNull(n, RoundingMode.DOWN);
  }

  public static ULong toULong(long n) {
    return toULongNonNull(n, RoundingMode.DOWN);
  }

  public static ULong toULong(float n) {
    return toULongNonNull(n, RoundingMode.DOWN);
  }

  public static ULong toULong(double n) {
    return toULongNonNull(n, RoundingMode.DOWN);
  }

  private static ULong toULongNonNull(Number n, RoundingMode mode) {
    if (n instanceof BigDecimal) {
      BigDecimal d = ((BigDecimal) n).setScale(0, mode);
      return Unsigned.ulong(d.toBigIntegerExact());
    } else if (n instanceof Byte || n instanceof Short || n instanceof Integer
        || n instanceof Long || n instanceof Float || n instanceof Double
        || n instanceof UByte || n instanceof UShort || n instanceof UInteger) {
      long value = n.longValue();
      if (value < 0) {
        throw new NumberFormatException("Value is out of range : " + value);
      }
      return Unsigned.ulong(value);
    } else if (n instanceof ULong) {
      return (ULong) n;
    }
    throw new IllegalArgumentException("Unexpected Number value " + n.getClass().getSimpleName());
  }

  public static @Nullable ULong toULong(@Nullable Number n, RoundingMode mode) {
    if (n == null) {
      return null;
    }
    return toULongNonNull(n, mode);
  }

  public static @Nullable ULong toULong(@Nullable String s, RoundingMode unused) {
    if (s == null) {
      return null;
    }
    return Unsigned.ulong(s);
  }
  public static @Nullable UShort toUShort(Object o) {
    if (o == null) {
      return null;
    }
    if (o instanceof Number) {
      return UShort.valueOf(((Number) o).intValue());
    }
    throw new IllegalArgumentException("Cannot cast to UShort: " + o.getClass());
  }

  public static @Nullable UByte toUByte(Object o) {
    if (o == null) {
      return null;
    }
    if (o instanceof Number) {
      return UByte.valueOf(((Number) o).shortValue());
    }
    throw new IllegalArgumentException("Cannot cast to UByte: " + o.getClass());
  }

  public static @Nullable UInteger toUInteger(Object o) {
    if (o instanceof Number) {
      return UInteger.valueOf(((Number) o).longValue());
    }
    throw new IllegalArgumentException("Cannot cast to UInteger: " + o.getClass());
  }

  public static @Nullable ULong toULong(Object o) {
    if (o instanceof Number) {
      return ULong.valueOf(((Number) o).longValue());
    }
    throw new IllegalArgumentException("Cannot cast to ULong: " + o.getClass());
  }

}
